Skip to content

Add processed_mentions tracking to prevent duplicate replies and improve fallback message - #110

Open
groupthinking wants to merge 1 commit into
mainfrom
fix-spam
Open

Add processed_mentions tracking to prevent duplicate replies and improve fallback message#110
groupthinking wants to merge 1 commit into
mainfrom
fix-spam

Conversation

@groupthinking

@groupthinking groupthinking commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Re-cut from current main (per the review decision to re-cut rather than hand-resolve six months of branch drift) and re-applies the original intent:

  • listener.py: persist replied-to mention IDs to XMCP_PROCESSED_MENTIONS_PATH and skip them on re-fetch — the inclusive start_time watermark re-returns the boundary mention on every restart, causing duplicate replies. The watermark still advances on skips, the ledger is compacted to XMCP_MAX_PROCESSED_MENTIONS entries on load, an unreadable ledger degrades to an empty set instead of killing the listener thread, and persistence failures are logged distinctly from reply failures.
  • agents/team/general.py: replace the "Thinking..." placeholder published when Grok returns nothing with an apology fallback (where the original fallback-message change now lives after the agent-team refactor).
  • env.example: document the new state variables.

The original TIMELINE_API_URL port change (8080 → 8000) is dropped per review feedback — the rest of the repo defaults to 8080.

Copilot AI lite review requested due to automatic review settings July 24, 2026 22:43
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary

  • Added bounded persistent processed_mentions tracking to prevent duplicate replies across restarts.
  • Added configuration for the ledger path and retained mention limit.
  • Changed the Grok failure fallback from "Thinking..." to an explicit retry message.
  • Updated timeline API defaults from port 8080 to 8000.
  • Removed an obsolete comment near the X 402 Payment Required backoff logic.

Walkthrough

listener.py adds a bounded persistent ledger for processed mentions. It skips duplicate IDs, compacts old entries, and records completed replies. Read and write failures are handled separately. Failed grok_chat processing now returns an explicit retry message.

Changes

Mention processing

Layer / File(s) Summary
Processed mention storage
listener.py, env.example
Adds configurable ledger storage, a 10,000-ID default limit, startup loading, oversized-ledger compaction, and append support.
Mention polling and reply completion
listener.py, agents/team/general.py
Skips processed IDs while advancing the watermark. New completed mentions are persisted. Persistence failures are logged without marking sent replies as failed. Failed processing returns a retry message instead of "Thinking...".

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant X_API
  participant listener
  participant Grok
  participant processed_mentions_file
  X_API->>listener: provide mentions
  listener->>listener: skip processed IDs
  listener->>Grok: request reply
  Grok-->>listener: reply or failure
  listener->>X_API: create reply tweet
  listener->>processed_mentions_file: persist mention ID
Loading

Possibly related PRs

Suggested labels: copilot-rabbit

Suggested reviewers: copilot

Poem

IDs pile up. The ledger groans.
Old entries get cut to bones.
Failed replies now say “retry.”
Writes can fail after replies fly.
Duplicate mentions meet the gate.

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Enforce Copilot Verification ❓ Inconclusive Pending verification of an explicit GitHub Copilot approval on PR #110. Verify PR #110 reviews and approval state from GitHub metadata; human comments or reviewer assignment do not satisfy this check.
Require Ai Unit Tests ❓ Inconclusive I am checking the repository and PR metadata before deciding. Need evidence for the copilot-rabbit label and committed AI-generated unit tests.
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies processed mention tracking and the fallback message change.
Description check ✅ Passed The description directly explains the ledger, duplicate-reply prevention, fallback behavior, configuration, and dropped port change.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-spam
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix-spam

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Sentry


Linked repositories: Public OSS repositories can only analyze public repositories installed in this organization. No linked repositories were analyzed; skipped groupthinking/uvai-skills.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds persistent tracking of processed X mention IDs in the Python listener to prevent duplicate replies across polling cycles/restarts, and updates the fallback reply message when Grok generation fails.

Changes:

  • Persist processed mention IDs to a local file and skip already-processed mentions.
  • Update the Grok error fallback reply text.
  • Change the listener’s default Timeline API URL (currently to port 8000).
Comments suppressed due to low confidence (1)

listener.py:100

  • The default TIMELINE_API_URL port was changed to 8000 here, but the rest of the repo (env.example, docker-compose, mcp_dispatcher.py) still defaults the timeline server to 8080. If TIMELINE_API_URL isn’t set, agent registration will POST to the wrong local endpoint.
    timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8000")

Comment thread listener.py Outdated

def push_timeline_card(title: str, body: str, metadata: dict) -> None:
timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8080")
timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8000")
Comment thread listener.py Outdated
Comment on lines +156 to +160
mention_id_str = str(mention.id)
if mention_id_str in processed_mentions:
print(f"Skipping already processed mention {mention.id}", flush=True)
continue

Comment thread listener.py Outdated
Comment on lines 176 to 184
try:
client.create_tweet(
text=grok_reply[:280],
in_reply_to_tweet_id=mention.id,
)
processed_mentions.add(mention_id_str)
save_processed_mention(mention_id_str)
except Exception as exc:
print(f"Error replying to mention {mention.id}: {exc}", flush=True)
Comment thread listener.py Outdated
Comment on lines +38 to +42
def load_processed_mentions() -> set[str]:
if not PROCESSED_MENTIONS_PATH.exists():
return set()
with PROCESSED_MENTIONS_PATH.open("r", encoding="utf-8") as f:
return {line.strip() for line in f if line.strip()}
Comment thread listener.py Outdated
POLL_SECONDS = int(os.getenv("POLL_INTERVAL_SECONDS", "60"))
PAYMENT_REQUIRED_BACKOFF_SECONDS = int(os.getenv("X_PAYMENT_REQUIRED_BACKOFF_SECONDS", "900"))

PROCESSED_MENTIONS_PATH = Path(os.getenv("XMCP_PROCESSED_MENTIONS_PATH", "~/.xmcp/processed_mentions.txt")).expanduser()

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@listener.py`:
- Around line 45-48: Replace the unbounded append-only storage in
save_processed_mention with a bounded durable store, or add safe
compaction/retention tied to the listener’s replay window. Ensure compaction
preserves every mention ID still needed to prevent replay while removing older
entries, and keep startup loading bounded accordingly.
- Around line 171-174: Update the get_grok_reply flow in the mention handler so
failure-like return values such as “Missing XAI_API_KEY.” and “Thinking...” are
detected before create_tweet(). Route them through the existing apology fallback
and ensure they are not published or recorded as successful replies; preserve
the current exception handling behavior for raised errors.
- Around line 38-42: Update load_processed_mentions() to handle filesystem read
errors without silently returning an empty set: retry transient failures or
propagate an explicit health failure. Adjust main() so failures from
load_processed_mentions() are handled within the daemon polling lifecycle,
preventing the listener thread from terminating while preserving the fail-closed
behavior that avoids duplicate replies.
- Around line 176-182: The mention handler around create_tweet and
save_processed_mention must distinguish successful tweet delivery from durable
state persistence. Do not let save_processed_mention failures enter the “Error
replying” path or treat the tweet as unsent; make persistence failures retry or
fail closed, and only update processed_mentions consistently with a successful
durable commit to prevent replay after restart.
- Line 86: Align the TIMELINE_API_URL fallback consistently across listener.py,
env.example, and agents/base.py, using the same endpoint default everywhere
(preferably port 8080 to match the existing configuration). Update the listener
registration and timeline-card request paths and the documented example
together, or make the variable mandatory in all locations.
- Around line 156-159: Update the mention-processing flow around the
processed_mentions check so the persisted start_time/cursor advances
monotonically to the newest processed mention, rather than being overwritten for
each newest-first item. Track the maximum tweet ID or timestamp across handled
mentions and persist that value only after processing the batch, while
preserving the skip behavior for already processed mentions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4e25f4c6-1faa-49f0-b6cb-fd85c8c5c777

📥 Commits

Reviewing files that changed from the base of the PR and between 8005bb6 and feca528.

📒 Files selected for processing (1)
  • listener.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • groupthinking/uvai-skills (manual)
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: copilot-pull-request-reviewer
🧰 Additional context used
🔍 Remote MCP GitHub Copilot

Additional PR context

  • PR #110 is open, has 1 commit, 1 changed file (listener.py), with +26/-4 lines; mergeable state is dirty. An auto-generated CodeRabbit comment says review is still in progress.
  • main’s listener.py already persists last_seen and processes mentions oldest-first; this PR adds a separate persisted processed_mentions set via XMCP_PROCESSED_MENTIONS_PATH, loads it at startup, skips already-processed mention IDs, and saves IDs after a successful reply.
  • The Grok-failure fallback text changes from Processing your tag... (error generating full response) to Sorry, I'm having trouble processing that. Try again or DM me.
  • The timeline API default in listener.py changes to http://127.0.0.1:8000, but env.example and agents/base.py still default TIMELINE_API_URL to http://127.0.0.1:8080; env.example also sets MCP_PORT=8000 and MCP_SERVER_URL=http://127.0.0.1:8000/mcp.
🔇 Additional comments (1)
listener.py (1)

19-19: LGTM!

Also applies to: 129-130, 140-141

Comment thread listener.py Outdated
Comment on lines +38 to +42
def load_processed_mentions() -> set[str]:
if not PROCESSED_MENTIONS_PATH.exists():
return set()
with PROCESSED_MENTIONS_PATH.open("r", encoding="utf-8") as f:
return {line.strip() for line in f if line.strip()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not let a broken state file kill the listener.

load_processed_mentions() lets filesystem errors escape, and main() calls it outside the polling error handler. Because main.py:49-58 runs this in a daemon thread, an unreadable or temporarily unavailable file terminates mention processing. Retry or fail closed with an explicit health failure; never silently use an empty set, since that re-enables duplicate replies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 38 - 42, Update load_processed_mentions() to handle
filesystem read errors without silently returning an empty set: retry transient
failures or propagate an explicit health failure. Adjust main() so failures from
load_processed_mentions() are handled within the daemon polling lifecycle,
preventing the listener thread from terminating while preserving the fail-closed
behavior that avoids duplicate replies.

Comment thread listener.py
Comment thread listener.py Outdated

def push_timeline_card(title: str, body: str, metadata: dict) -> None:
timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8080")
timeline_url = os.getenv("TIMELINE_API_URL", "http://127.0.0.1:8000")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align the timeline endpoint default everywhere.

listener.py now defaults to port 8000, while env.example and agents/base.py still default 8080. With no explicit TIMELINE_API_URL, registration and timeline-card requests hit the wrong service. Update all defaults together or require the variable explicitly.

Also applies to: 100-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` at line 86, Align the TIMELINE_API_URL fallback consistently
across listener.py, env.example, and agents/base.py, using the same endpoint
default everywhere (preferably port 8080 to match the existing configuration).
Update the listener registration and timeline-card request paths and the
documented example together, or make the variable mandatory in all locations.

Comment thread listener.py Outdated
Comment thread listener.py Outdated
Comment on lines +171 to +174
grok_reply = get_grok_reply(prompt)
except Exception as exc:
print(f"Error getting Grok reply for mention {mention.id}: {exc}", flush=True)
grok_reply = "Processing your tag... (error generating full response)"
grok_reply = "Sorry, I'm having trouble processing that. Try again or DM me."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Route all failed Grok results through the apology fallback.

get_grok_reply() returns "Missing XAI_API_KEY." and "Thinking..." for failure-like paths instead of raising. This handler therefore publishes and records those placeholders as successful replies. Return a typed success/error result or raise for these cases before calling create_tweet().

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 172-172: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 171 - 174, Update the get_grok_reply flow in the
mention handler so failure-like return values such as “Missing XAI_API_KEY.” and
“Thinking...” are detected before create_tweet(). Route them through the
existing apology fallback and ensure they are not published or recorded as
successful replies; preserve the current exception handling behavior for raised
errors.

Comment thread listener.py Outdated
Comment on lines +176 to +182
try:
client.create_tweet(
text=grok_reply[:280],
in_reply_to_tweet_id=mention.id,
)
processed_mentions.add(mention_id_str)
save_processed_mention(mention_id_str)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not treat state-persistence failure as tweet failure.

If create_tweet() succeeds but save_processed_mention() fails, this block logs “Error replying” after already sending the reply. The ID is also added to memory before the disk write, so only the current process suppresses a duplicate; after restart, the reply is sent again. Separate the external write from the durable state commit and retry/fail closed on persistence errors instead of blindly replaying a non-idempotent tweet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 176 - 182, The mention handler around create_tweet
and save_processed_mention must distinguish successful tweet delivery from
durable state persistence. Do not let save_processed_mention failures enter the
“Error replying” path or treat the tweet as unsent; make persistence failures
retry or fail closed, and only update processed_mentions consistently with a
successful durable commit to prevent replay after restart.

@groupthinking

Copy link
Copy Markdown
Owner Author

Do not resolve the conflicts on this PR. Re-cut the branch from current main instead.

I audited all five open PRs (#106#110) against the GitHub compare API. Every one of them is a long-stale branch, not a genuine content conflict:

PR Head branch Ahead Behind Merge base
#106 claude/myxstack-agent-merge-myaock 12 33 2026-04-20
#107 copilot/add-workflow-files 5 103 2026-01-28
#108 copilot/clear-pending-prs 4 107 2026-01-28
#109 copilot/clear-pending-prs-again 4 117 2026-01-27
#110 fix-spam 1 53 2026-02-12

All five report status=diverged.

Why the diffs look absurd

#109 is titled "fix: replace invalid CODEOWNERS entries with @groupthinking" but reports +30,880 / −216 across 38 files, with essentially every file showing +N / −0 — including openapi.json (+23,095), package-lock.json (+1,187), and MOLT_STRATEGIC_ANALYSIS.md (+1,075).

That is not what the PR intends to change. The branch was cut on 2026-01-27 and opened as a PR on 2026-07-24 — a six-month gap during which main advanced 117 commits. The diff is being computed against that January merge base, so it renders the branch's stale snapshot of the tree as bulk additions.

The conflicts are therefore real, but they are drift, not disagreement. Resolving them by hand means manually reconciling six months of divergence across 38 files to land what should be a handful of line edits — and every manual resolution is an opportunity to silently revert work that landed on main in the interim.

Resolution

For each PR, the actual intent is small and is captured in only 1–12 commits. Re-apply that intent on top of current main:

git fetch origin
git checkout -b <name>-rebased origin/main
git cherry-pick <the 1-12 real commits>   # or simply re-make the edit by hand

Then open a replacement PR and close the stale one. For #109 specifically, the intended change is a CODEOWNERS edit — that is a few lines, and re-making it by hand against current main is strictly faster and safer than reconciling a 30k-line diff.

Recommended disposition:

Root cause to fix going forward: these five PRs were all opened within a 112-second window (22:41:30 → 22:43:22 on 2026-07-24) from branches that were months old. Whatever automation opened them did not rebase first. Adding a staleness check — refuse to open a PR whose branch is more than N commits behind its base — would prevent this class of PR entirely.

…lback reply

Re-applies the intent of the original fix-spam commit on top of current
main, per the review decision to re-cut rather than hand-resolve six
months of branch drift.

- listener.py: persist replied-to mention IDs (XMCP_PROCESSED_MENTIONS_PATH)
  and skip them on re-fetch — the inclusive start_time watermark re-returns
  the boundary mention on every restart. The watermark still advances on
  skips, the ledger is compacted to XMCP_MAX_PROCESSED_MENTIONS on load,
  an unreadable ledger degrades to an empty set instead of killing the
  listener thread, and persistence failures are logged distinctly from
  reply failures.
- agents/team/general.py: replace the "Thinking..." placeholder published
  when Grok returns nothing with an apology fallback.
- env.example: document the new state variables.

The original commit's TIMELINE_API_URL port change (8080 -> 8000) is
dropped per review feedback — the rest of the repo defaults to 8080.

Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 4, 2026 06:06
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🔍 PR Validation

❌ PR description is required (minimum 20 characters)
⚠️ PR title should follow conventional commits format

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🔍 PR Validation

⚠️ PR title should follow conventional commits format

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (1)

listener.py:275

  • processed_mentions is only compacted on startup, but it grows for the entire lifetime of the listener (processed_mentions.add(...) on every successful mention). In a long-running process this can grow without bound (and contradicts the intent in the comment that the ledger only needs to cover a replay window). The TypeScript agent (src/services/agent.ts:101-107) prunes its processed set to avoid this.

Consider bounding the in-memory set here (and optionally add periodic on-disk compaction if the ledger file is expected to grow large between restarts).

                processed_mentions.add(mention_id)
                try:
                    save_processed_mention(mention_id)
                except OSError as exc:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
listener.py (1)

76-79: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Ledger only compacts at startup; a long-running process still grows it unbounded.

save_processed_mention() unconditionally appends. Compaction against MAX_PROCESSED_MENTIONS only runs inside load_processed_mentions(), called once in main(). Between restarts, this daemon thread's ledger file grows without limit — the documented cap only takes effect the next time the process restarts. This is the same unbounded-growth concern already raised, now only half-fixed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 76 - 79, Update save_processed_mention so the
processed-mentions ledger is compacted during runtime, not only by
load_processed_mentions at startup. After appending the new mention ID, enforce
MAX_PROCESSED_MENTIONS by retaining only the newest allowed entries, while
preserving the existing file format and append behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@listener.py`:
- Around line 45-66: Update load_processed_mentions so an OSError while reading
PROCESSED_MENTIONS_PATH does not return an empty processed-mentions set; retry
the ledger read or fail closed by preserving duplicate suppression. Adjust the
docstring and warning to reflect the chosen behavior, while keeping normal
loading and compaction unchanged.
- Around line 265-275: Update the exception path around save_processed_mention
in the mention-processing flow so an OSError does not allow start_time to
advance past an unpersisted mention; stop or otherwise retry processing before
advancing the checkpoint, while preserving the existing warning context and
normal success behavior.
- Around line 67-68: Update the truncation logic near MAX_PROCESSED_MENTIONS to
handle zero and negative limits explicitly: a non-positive cap must produce an
empty lines list, while positive caps retain only the last
MAX_PROCESSED_MENTIONS entries when the list exceeds the limit.
- Around line 15-23: Ensure load_env() runs before PROCESSED_MENTIONS_PATH and
MAX_PROCESSED_MENTIONS are evaluated, or resolve both settings lazily after
environment loading in main(). Preserve the existing environment variable names
and defaults so .env values control the ledger path and cap.

---

Duplicate comments:
In `@listener.py`:
- Around line 76-79: Update save_processed_mention so the processed-mentions
ledger is compacted during runtime, not only by load_processed_mentions at
startup. After appending the new mention ID, enforce MAX_PROCESSED_MENTIONS by
retaining only the newest allowed entries, while preserving the existing file
format and append behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dff295e5-dc01-408d-a561-97665eecf8f5

📥 Commits

Reviewing files that changed from the base of the PR and between feca528 and 763c794.

📒 Files selected for processing (3)
  • agents/team/general.py
  • env.example
  • listener.py
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: copilot-pull-request-reviewer
🧰 Additional context used
🪛 ast-grep (0.45.0)
listener.py

[warning] 105-105: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.post(f"{timeline_url}/v1/timeline/items", json=payload, timeout=10)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)

🪛 Ruff (0.16.0)
agents/team/general.py

[warning] 8-8: typing.Dict is deprecated, use dict instead

(UP035)


[warning] 22-22: Missing return type annotation for special method __init__

Add return type annotation: None

(ANN204)

listener.py

[warning] 134-134: Do not catch blind exception: Exception

(BLE001)


[warning] 157-157: Consider moving this statement to an else block

(TRY300)


[warning] 158-158: Do not catch blind exception: Exception

(BLE001)


[warning] 164-164: Do not catch blind exception: Exception

(BLE001)


[warning] 176-176: Do not catch blind exception: Exception

(BLE001)


[warning] 195-195: Do not catch blind exception: Exception

(BLE001)

🔍 Remote MCP GitHub Copilot

Relevant review context

  • The PR’s current diff only changes listener.py, agents/team/general.py, and env.example; no tests cover the new ledger behavior.
  • PROCESSED_MENTIONS_PATH and MAX_PROCESSED_MENTIONS are evaluated at module import time, while load_env() runs later inside main(). Therefore values supplied only through .env may not configure the new ledger.
  • MAX_PROCESSED_MENTIONS=0 is not safely bounded: lines[-0:] retains all entries, so the documented maximum can be bypassed.
  • The ledger is appended after processing, while save_last_seen() follows afterward. A crash between those writes can leave the mention recorded as processed but the watermark unchanged; the next poll will skip it and then advance the watermark. This is consistent with the intended duplicate suppression but should be covered by tests.
  • The PR branch is reported as 53 commits behind main and divergent; the author explicitly recommends re-cutting it from current main rather than resolving conflicts manually.
  • The automated review reported no Copilot approval, no AI unit-test label/tests for this PR, and an inconclusive description check.
🔇 Additional comments (5)
listener.py (2)

95-109: LGTM!


112-160: LGTM!

env.example (1)

45-47: LGTM!

agents/team/general.py (2)

34-55: LGTM!


57-70: LGTM!

Comment thread listener.py
Comment on lines 15 to +23
LAST_SEEN_PATH = Path(os.getenv("XMCP_LAST_SEEN_PATH", "~/.xmcp/last_seen.txt")).expanduser()
PROCESSED_MENTIONS_PATH = Path(
os.getenv("XMCP_PROCESSED_MENTIONS_PATH", "~/.xmcp/processed_mentions.txt")
).expanduser()
# The last-seen watermark has second granularity and start_time is inclusive,
# so the newest processed mention is re-fetched on every restart. The
# processed-mentions ledger exists to suppress that duplicate reply; it only
# needs to cover the replay window, not all history.
MAX_PROCESSED_MENTIONS = int(os.getenv("XMCP_MAX_PROCESSED_MENTIONS", "10000"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
fd -a 'listener.py|env.example|\.env' . 2>/dev/null || true

echo "== listener outline/size =="
if [ -f listener.py ]; then
  wc -l listener.py
  ast-grep outline listener.py --view compact || true
  echo "== relevant listener.py =="
  sed -n '1,35p;180,225p;260,330p' listener.py
fi

echo "== load_env usages/definition =="
rg -n "def load_env|load_env\(|os\.getenv\(|XMCP_PROCESSED_MENTIONS_PATH|XMCP_MAX_PROCESSED_MENTIONS|XMCP_LAST_SEEN_PATH" . -g '!node_modules' -g '!build' -g '!dist' || true

echo "== env docs =="
if [ -f env.example ]; then
  sed -n '1,160p' env.example
fi

Repository: groupthinking/MyXstack

Length of output: 11637


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

listener = Path("listener.py").read_text()
tree = ast.parse(listener)

module_os_getenv_calls = []
load_env_body = []
main_load_env_calls = []
main_body = []

for node in ast.iter_child_nodes(tree):
    if isinstance(node, ast.Assign):
        if any(isinstance(t, ast.Name) and t.id == "LAST_SEEN_PATH" for t in node.targets
               for t in node.targets if isinstance(t, ast.Name)) or \
           any(isinstance(t, ast.Name) and t.id == "PROCESSED_MENTIONS_PATH" for t in node.targets
               for t in node.targets if isinstance(t, ast.Name)) or \
           any(isinstance(t, ast.Name) and t.id == "MAX_PROCESSED_MENTIONS" for t in node.targets
               for t in node.targets if isinstance(t, ast.Name)):
            for child in ast.walk(node):
                if isinstance(child, ast.Call) and isinstance(child.func, ast.Attribute):
                    if child.func.attr == "getenv":
                        module_os_getenv_calls.append((node.lineno, child.func.value.id, child.func.attr))
    if isinstance(node, ast.FunctionDef) and node.name == "load_env":
        load_env_body.extend([child.lineno for child in ast.walk(node) if hasattr(child, "lineno")])
    if isinstance(node, ast.FunctionDef) and node.name == "main":
        main_load_env_calls.extend((start.lineno, child.lineno)
                                   for start in node.body[:1] if isinstance(start, ast.Expr) and isinstance(start.value, ast.Call) and isinstance(start.value.func, ast.Name) and start.value.func.id == "load_env"
                                   for child in [start])
        main_body.extend([child.lineno for child in ast.walk(node) if hasattr(child, "lineno")])

print("module-level os.getenv calls:", module_os_getenv_calls)
print("load_env function line range approximately:", min(load_env_body) if load_env_body else None, max(load_env_body) if load_env_body else None)
print("main load_env call lines:", [lines[1] for lines in main_load_env_calls])
print("main lines:", min(main_body) if main_body else None, max(main_body) if main_body else None)
print("module-level os.getenv before load_env:", all(l < min(load_env_body) for _, _, l in module_os_getenv_calls))
print("main load_env lines:", sorted(main_load_env_calls))
PY

Repository: groupthinking/MyXstack

Length of output: 539


.env-only ledger config is ignored.

PROCESSED_MENTIONS_PATH and MAX_PROCESSED_MENTIONS are bound at import time. load_env() runs later in main(), so .env values for these keys do not affect the ledger path or cap. Move load_env() before module-level config reads, or resolve these values lazily/inside main().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 15 - 23, Ensure load_env() runs before
PROCESSED_MENTIONS_PATH and MAX_PROCESSED_MENTIONS are evaluated, or resolve
both settings lazily after environment loading in main(). Preserve the existing
environment variable names and defaults so .env values control the ledger path
and cap.

Comment thread listener.py
Comment on lines +45 to +66
def load_processed_mentions() -> "set[str]":
"""Load recently processed mention IDs, compacting the ledger on the way.

An unreadable ledger must not kill the listener thread — worst case a
few boundary mentions get a second reply, which is preferable to no
mentions being handled at all.
"""
try:
if not PROCESSED_MENTIONS_PATH.exists():
return set()
lines = [
line.strip()
for line in PROCESSED_MENTIONS_PATH.read_text(encoding="utf-8").splitlines()
if line.strip()
]
except OSError as exc:
print(
f"WARNING: could not read {PROCESSED_MENTIONS_PATH}: {exc}; "
"starting with an empty processed-mentions set (duplicate replies possible)",
flush=True,
)
return set()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail-open on ledger read errors re-enables the exact duplicate-reply risk already flagged.

This still returns an empty set on any OSError, meaning a transient or permanent read failure resets duplicate suppression to zero and lets every boundary mention get replied to again. The docstring says this is intentional, but it's the same problem previously raised: retry or fail closed instead of quietly reopening the door to duplicate replies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 45 - 66, Update load_processed_mentions so an
OSError while reading PROCESSED_MENTIONS_PATH does not return an empty
processed-mentions set; retry the ledger read or fail closed by preserving
duplicate suppression. Adjust the docstring and warning to reflect the chosen
behavior, while keeping normal loading and compaction unchanged.

Comment thread listener.py
Comment on lines +67 to +68
if len(lines) > MAX_PROCESSED_MENTIONS:
lines = lines[-MAX_PROCESSED_MENTIONS:]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

MAX_PROCESSED_MENTIONS=0 doesn't cap anything.

lines[-0:] is lines[0:] — the whole list. Setting the cap to 0 silently disables truncation instead of shrinking it to zero, and a negative value slices from the wrong end. Guard the boundary explicitly.

🔧 Proposed fix
-    if len(lines) > MAX_PROCESSED_MENTIONS:
-        lines = lines[-MAX_PROCESSED_MENTIONS:]
+    if MAX_PROCESSED_MENTIONS <= 0:
+        lines = []
+    elif len(lines) > MAX_PROCESSED_MENTIONS:
+        lines = lines[-MAX_PROCESSED_MENTIONS:]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(lines) > MAX_PROCESSED_MENTIONS:
lines = lines[-MAX_PROCESSED_MENTIONS:]
if MAX_PROCESSED_MENTIONS <= 0:
lines = []
elif len(lines) > MAX_PROCESSED_MENTIONS:
lines = lines[-MAX_PROCESSED_MENTIONS:]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 67 - 68, Update the truncation logic near
MAX_PROCESSED_MENTIONS to handle zero and negative limits explicitly: a
non-positive cap must produce an empty lines list, while positive caps retain
only the last MAX_PROCESSED_MENTIONS entries when the list exceeds the limit.

Comment thread listener.py
Comment on lines +265 to +275
processed_mentions.add(mention_id)
try:
save_processed_mention(mention_id)
except OSError as exc:
# The reply already went out — a persistence failure only
# risks a duplicate after restart, so log it as such
# rather than as a reply failure.
print(
f"WARNING: could not persist processed mention {mention.id}: {exc}",
flush=True,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Persistence failure here still produces a duplicate reply after restart.

When save_processed_mention() raises OSError, the code logs a warning and continues; start_time still advances past this mention right after. On restart, load_last_seen() resumes exactly at this mention, load_processed_mentions() does not contain its ID (the write failed), and it gets replied to a second time. This reproduces the previously flagged risk of treating a persistence failure as harmless when the reply already went out non-idempotently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@listener.py` around lines 265 - 275, Update the exception path around
save_processed_mention in the mention-processing flow so an OSError does not
allow start_time to advance past an unpersisted mention; stop or otherwise retry
processing before advancing the checkpoint, while preserving the existing
warning context and normal success behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants